﻿/*
ChangeTracking (a library for POCO change tracking).

Copyright (C) 2014 Dmitry Merzagitovich Arslanov

This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 2.1 of the License, or (at your option) any
later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*/

namespace ChangeTracking
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Arc = Digraph.Arc<object, Property>;
    using Difference = Digraph.Difference<object, Property>;
    using Graph = Digraph.Set<object, Property>;

    /// <summary>
    /// Changes tracking context. Creating of new instance begin changes
    /// tracking. Disposing of transaction stop tracking. All changes are made
    /// during tracking are rolled back if transaction is not commited.
    /// Changes are submitted in log by transaction commiting. User can undo
    /// and redo changes in log.
    /// </summary>
    public sealed class Transaction : IDisposable
    {
        private ChangeTracker tracker;

        private ObjectGraphBuilder graphBuilder;

        /// <summary>
        /// Object for which changes are tracked.
        /// </summary>
        public object TrackedObject { get; private set; }

        /// <summary>
        /// Object graph before changes.
        /// </summary>
        public Graph ObjectGraphBeforeChanges { get; private set; }

        /// <summary>
        /// Object graph after changes.
        /// </summary>
        public Graph ObjectGraphAfterChanges { get; private set; }

        /// <summary>
        /// Difference between graphs before and after changes.
        /// </summary>
        public Difference ObjectGraphDifference { get; private set; }

        /// <summary>
        /// Indicates that transaction is disposed.
        /// </summary>
        public bool IsDisposed { get; private set; }

        /// <summary>
        /// Indicates that transaction is commited.
        /// </summary>
        public bool IsCommited { get; private set; }

        /// <summary>
        /// Description of changes are made during tracking.
        /// </summary>
        public string Description { get; private set; }

        internal Transaction(ChangeTracker tracker,
                             object trackedObject)
        {
            this.tracker = tracker;
            TrackedObject = trackedObject;
            graphBuilder = new ObjectGraphBuilder(TrackedObject);
            ObjectGraphBeforeChanges = graphBuilder.Build();
        }

        ~Transaction()
        {
            Dispose(false);
        }

        private void Dispose(bool disposing)
        {
            if (!IsDisposed)
            {
                ObjectGraphAfterChanges = graphBuilder.Build();
                ObjectGraphDifference = Graph.Compare(ObjectGraphBeforeChanges,
                                                      ObjectGraphAfterChanges);
                ChangeSet chgs = GraphDiffToChangeSet(ObjectGraphDifference);
                if (chgs != null)
                {
                    if (IsCommited) tracker.SubmitChanges(chgs, Description);
                    else chgs.Undo();
                }
                IsDisposed = true;
            }
        }

        /// <summary>
        /// Stop changes tracking.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Commit transaction and stop tracking.
        /// </summary>
        /// <param name="description">
        /// Description of changes made during tracking.
        /// </param>
        public void Commit(string description)
        {
            if (!IsDisposed)
            {
                Description = description;
                IsCommited = true;
                Dispose();
            }
        }

        private ChangeSet GraphDiffToChangeSet(Difference difference)
        {
            List<Change> changes = new List<Change>();
            foreach (Arc newArc in difference.AddedArcs)
            {
                ListProperty listProp = newArc.Content as ListProperty;
                if (listProp != null)
                {
                    AddToListChange change = new AddToListChange(
                                                    newArc.FromVertex.Content,
                                                    newArc.Content.Info,
                                                    listProp.List,
                                                    newArc.ToVertex.Content);
                    changes.Add(change);
                }
                else
                {
                    SingleProperty propChange =
                        newArc.Content as SingleProperty;
                    Arc delArc = difference.RemovedArcs.FirstOrDefault(
                        arc => arc.Content == newArc.Content &&
                            object.ReferenceEquals(arc.FromVertex.Content,
                                                   newArc.FromVertex.Content));
                    if (delArc != null)
                    {
                        PropertyChange change = new PropertyChange(
                                                    newArc.FromVertex.Content,
                                                    newArc.Content.Info,
                                                    delArc.ToVertex.Content,
                                                    newArc.ToVertex.Content);
                        changes.Add(change);
                    }
                }
            }
            foreach (Arc delArc in difference.RemovedArcs)
            {
                ListProperty listProp = delArc.Content as ListProperty;
                if (listProp != null)
                {
                    RemoveFromListChange change = new RemoveFromListChange(
                                                delArc.FromVertex.Content,
                                                delArc.Content.Info,
                                                listProp.List,
                                                delArc.ToVertex.Content);
                    changes.Add(change);
                }
            }
            return changes.Count > 0 ? new ChangeSet(changes) : null;
        }
    }
}
